package android.gpstrac.controls;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.gpstrac.data.ClientRequest;
import android.gpstrac.data.ClientRequest.RequestType;
import android.gpstrac.data.ServerResponse;
import android.gpstrac.data.ServerResponse.ResponseType;
import android.gpstrac.data.User;
import android.gpstrac.data.User.UserContent;
import android.gpstrac.data.UserLocation;
import android.gpstrac.data.UserLocation.UserLocationContent;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

/**
 * Background service which handles updates from the server.
 *
 * @author Nicolas Papin
 */
public class GPSTracUpdateService extends Service
{
	//
	// CLASS AND INSTANCE DATA
	//
	/**
	 * Tag for logging (for debugging).
	 */
	private static final String TAG = GPSTracUpdateService.class.getSimpleName();
	
	/**
	 * Timer to query to the server.
	 */
	private Timer mTimer;
	
	/**
	 * The username to use for database queries.
	 */
	private String mUsername;
	
	/**
	 * The running state of the system.
	 */
	private boolean mRunning = false;
	
	/**
	 * Server communication socket.
	 */
	Socket mSock;
	
	/**
	 * Some locks.
	 */
	private Object mRunLock = new Object();
	
	/**
	 * Time task.
	 */
	private TimerTask mUpdateTask = new TimerTask() 
	{
		@Override
		public void run()
		{
		  Log.i(TAG, "Updates Enabled: " + mRunning);
		  getUpdates();
		}
	};
	private final static int mRefetchDelay = 30;
	
	/**
	 * Location listener.
	 */
	private GPSTracLocationListener mListener;
	private int mMinUpdateTime = 30;
	private float mMinUpdateDistance = 50;
	
	/**
	 * Action receivers.
	 */
	private StartStopReceiver mStartStop;
	private UpdateUserReceiver mUpdateUser;
	private UpdateLocationReceiver mLocReceiver;
	private AddFriendReceiver mFriendReceiver;
	
	//
	// METHODS
	//
	/* (non-Javadoc)
	 * @see android.app.Service#onBind(android.content.Intent)
	 */
	@Override
	public IBinder onBind(Intent intent)
	{
		return null;
	}
	
	@Override
	public void onCreate()
	{
		super.onCreate();
	 
		Log.i(TAG, "Service being created.");
		
		String name = GPSTracUpdateService.class.getName();
		
		mListener = new GPSTracLocationListener();
		LocationManager lm =  ((LocationManager)getSystemService(Context.LOCATION_SERVICE));
		
		Criteria crit = new Criteria();
		crit.setAccuracy(crit.getAccuracy());
		
		SharedPreferences settings = getSharedPreferences(GPSTracControls.PREFS, 0);
		
		synchronized (mRunLock)
		{
			mUsername = settings.getString(GPSTracControls.UNPREF, "");
			mRunning = settings.getBoolean(GPSTracControls.SPREF, false);
			
			if(mRunning)
				connect();
		}
		
		String provider = lm.getBestProvider(crit, true);
		lm.requestLocationUpdates(provider, mMinUpdateTime, mMinUpdateDistance, mListener);
		
		mStartStop = new StartStopReceiver();
		IntentFilter ssFilt = new IntentFilter(name + ".toggle");
		registerReceiver(mStartStop, ssFilt);
		
		mUpdateUser = new UpdateUserReceiver();
		IntentFilter uuFilt = new IntentFilter(name + ".username");
		registerReceiver(mUpdateUser, uuFilt);
		
		mLocReceiver = new UpdateLocationReceiver();
		IntentFilter luFilt = new IntentFilter(name + ".sendLoc");
		registerReceiver(mLocReceiver, luFilt);
		
		mFriendReceiver = new AddFriendReceiver();
		IntentFilter afFilt = new IntentFilter(name + ".addFriend");
		registerReceiver(mFriendReceiver, afFilt);
		
	    mTimer = new Timer("GPSTracUpdateTimer");
	    mTimer.schedule(mUpdateTask, 1000L, mRefetchDelay * 1000L);
	}
	
	@Override
	public void onDestroy() 
	{
	    super.onDestroy();
	 
	    Log.i(TAG, "Service being destroyed.");
	    
	    synchronized (mRunLock)
	    {
	    	disconnect();
	    }
	    	
	    mTimer.cancel();
	    mTimer = null;
	    
	    unregisterReceiver(mStartStop);
	    mStartStop = null;
	    
	    unregisterReceiver(mUpdateUser);
	    mUpdateUser = null;
	    
	    unregisterReceiver(mLocReceiver);
	    mLocReceiver = null;
	    
	    unregisterReceiver(mFriendReceiver);
	    mFriendReceiver = null;
	    
	    ((LocationManager)getSystemService(Context.LOCATION_SERVICE)).removeUpdates(mListener);
	    mListener = null;
	}
	
	private void getUpdates()
	{
		synchronized (mRunLock)
		{			
			ClientRequest cr;
			ServerResponse sr;
			User[] friends;
			UserLocation[][] friendLocations;
			
			if(mRunning && mSock != null && mSock.isConnected())
			{
				Log.i(TAG, "Polling server for updates.");
				cr = new ClientRequest(RequestType.GET_FRIENDS, 0);
				sr = new ServerResponse();
				int numNames = 0;
				
				ContentValues[] nameValues;
				ContentValues[][] locValues;
				
				try
				{
					send(cr.getServerRepresentation());
					sr.fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
					if(sr.getType() != ResponseType.REQ_OK)
						return;
					
					numNames = sr.getLen() / User.USER_ID_LEN;
					
					byte[][] frNames = divideByteArray(receive(sr.getLen()), User.USER_ID_LEN, numNames);
					friends = new User[numNames];
					friendLocations = new UserLocation[numNames][];
					
					nameValues = new ContentValues[numNames];
					locValues = new ContentValues[numNames][];
					
					for(int i = 0; i < numNames; i++)
					{
						friends[i] = new User();
						friends[i].fillWithTransmittedData(frNames[i]);
						nameValues[i] = friends[i].getValueHolder();
						
						cr = new ClientRequest(RequestType.LOC_HIST, User.USER_ID_LEN);
						send(cr.getServerRepresentation());
						(sr = new ServerResponse()).fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
						if(sr.getType() != ResponseType.REQ_OK)
							return;
						
						send(friends[i].getServerRepresentation());
						(sr = new ServerResponse()).fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
						if(sr.getType() != ResponseType.REQ_OK)
							return;
						
						int numCoords = sr.getLen() / UserLocation.COORD_LEN;
						friendLocations[i] = new UserLocation[numCoords];
						locValues[i] = new ContentValues[numCoords];
						
						byte[][] frLoc = divideByteArray(receive(sr.getLen()), UserLocation.COORD_LEN, numCoords);
						
						for(int j = 0; j < numCoords; j++)
						{
							friendLocations[i][j] = new UserLocation(friends[i]);
							friendLocations[i][j].fillWithTransmittedData(frLoc[j]);
							friendLocations[i][j].setSequence(j);
							locValues[i][j] = friendLocations[i][j].getValueHolder();
						}
						
						if(friendLocations[i].length > 0 && friendLocations[i][0].isInvalid())
							locValues[i] = new ContentValues[0];
					}
				}
				catch(Exception ex)
				{
					Log.i(TAG, "Failed to obtain updates from the server.");
					ex.printStackTrace();
					return;
				}
				
				getContentResolver().delete(UserLocationContent.CONTENT_URI, null, null);
				getContentResolver().delete(UserContent.CONTENT_URI, null, null);
				
				getContentResolver().bulkInsert(UserContent.CONTENT_URI, nameValues);
				for(int i = 0; i < locValues.length; i++)
				{
					getContentResolver().bulkInsert(UserLocationContent.CONTENT_URI, locValues[i]);
				}
			}
		}
	}
	
	private void sendLocationUpdate(Location loc)
	{
		UserLocation ul = new UserLocation(loc.getLatitude(), loc.getLongitude());
		
		synchronized (mRunLock)
		{
			if(mRunning && mSock != null && mSock.isConnected())
			{
				Log.i(TAG, "Updating server with new location " + loc.getLatitude() + ", " + loc.getLongitude() + ".");
				ClientRequest cr = new ClientRequest(RequestType.UPDATE_LOC, UserLocation.COORD_LEN);
				ServerResponse sr = new ServerResponse();
				
				try
				{
					send(cr.getServerRepresentation());
					sr.fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
					if(sr.getType() != ResponseType.REQ_OK)
						return;
					
					send(ul.getServerRepresentation());
					(sr = new ServerResponse()).fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
					if(sr.getType() != ResponseType.REQ_OK)
						return;
				}
				catch(Exception ex)
				{
					Log.i(TAG, "Failed to send location update to the server.");
					ex.printStackTrace();
					return;
				}
			}
		}
		
		Log.i(TAG, "New location accepted by server.");
	}
	
	private void connect()
	{
		if(!mUsername.equals(""))
		{
			try
			{
				Log.i(TAG, "Logging into server as " + mUsername + ".");
				
	    		InetAddress servAddr = Inet4Address.getByName("10.0.2.2");
				mSock = new Socket(servAddr, 31337);
				
				User u = new User(mUsername);
				ClientRequest cr = new ClientRequest(RequestType.LOG_IN, User.USER_ID_LEN);
				ServerResponse sr = new ServerResponse();
				
				send(cr.getServerRepresentation());
				sr.fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
				if(sr.getType() != ResponseType.REQ_OK)
					return;
			
				send(u.getServerRepresentation());
				sr.fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
				if(sr.getType() != ResponseType.REQ_OK)
					return;
				
				Log.i(TAG, "Successfully logged into server.");
			}
			catch (Exception ex)
			{
				Log.i(TAG, "Problem while logging into server.");
				ex.printStackTrace();
			}
		}
	}
	
	private void disconnect()
	{
		try
		{
	    	if(!mSock.isClosed())
	    	{
	    		Log.i(TAG, "Logging out of server.");
	    		ClientRequest cr = new ClientRequest(RequestType.LEAVE, 0);
				ServerResponse sr = new ServerResponse();
	    	
				send(cr.getServerRepresentation());
				sr.fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
				
	    		mSock.close();
	    	}
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
		
		mSock = null;
	}
	
	private void send(byte[] data) throws Exception
	{
		DataOutputStream dos = new DataOutputStream(mSock.getOutputStream());
		dos.write(data, 0, data.length);
	}
	
	private byte[] receive(int amount) throws Exception
	{
		byte[] dataBuff = new byte[amount];
		
		DataInputStream dis = new DataInputStream(mSock.getInputStream());
		dis.read(dataBuff, 0, amount);
		
		return dataBuff;
	}
	
	private byte[][] divideByteArray(byte[] data, int len, int num)
	{
		byte[][] out = new byte[num][len];
		
		if(data.length < (len * num))
			return null;
		
		for(int i = 0; i < num; i++)
			for(int j = 0; j < len; j++)
			{
				out[i][j] = data[i*len + j];
			}
		
		return out;
	}
	
	//
	// PRIVATE CLASSES
	//
	private class GPSTracLocationListener implements LocationListener
	{

		/* (non-Javadoc)
		 * @see android.location.LocationListener#onLocationChanged(android.location.Location)
		 */
		@Override
		public void onLocationChanged(Location location)
		{
			sendLocationUpdate(location);
		}

		/* (non-Javadoc)
		 * @see android.location.LocationListener#onProviderDisabled(java.lang.String)
		 */
		@Override
		public void onProviderDisabled(String provider){}

		/* (non-Javadoc)
		 * @see android.location.LocationListener#onProviderEnabled(java.lang.String)
		 */
		@Override
		public void onProviderEnabled(String provider){}

		/* (non-Javadoc)
		 * @see android.location.LocationListener#onStatusChanged(java.lang.String, int, android.os.Bundle)
		 */
		@Override
		public void onStatusChanged(String provider, int status, Bundle extras){}
		
	}
	
	private class StartStopReceiver extends BroadcastReceiver
	{
		
		/* (non-Javadoc)
		 * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
		 */
		@Override
		public void onReceive(Context context, Intent intent)
		{
			SharedPreferences settings = getSharedPreferences(GPSTracControls.PREFS, 0);
			
			Log.i(TAG, "Request to start/stop server updates recieved.");
			synchronized (mRunLock)
			{
				mRunning = settings.getBoolean(GPSTracControls.SPREF, false);
				
				if(mRunning && mSock == null)
				{
					connect();
				}
				else if(!mRunning && mSock != null)
				{
					disconnect();
				}
			}
		}
		
	}
	
	private class UpdateUserReceiver extends BroadcastReceiver
	{
		
		/* (non-Javadoc)
		 * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
		 */
		@Override
		public void onReceive(Context context, Intent intent)
		{			
			SharedPreferences settings = getSharedPreferences(GPSTracControls.PREFS, 0);
			Log.i(TAG, "Username change request received.");
			
			synchronized (mRunLock)
			{
				String newName = settings.getString(GPSTracControls.UNPREF, "");
				if(newName.equals(mUsername))
				{
					Log.i(TAG, "Username unchanged.");
					return;
				}
				
				mUsername = newName;
					
				// Disconnect and clear data
				if(mRunning || mSock != null)
					disconnect();
				
				getContentResolver().delete(UserLocationContent.CONTENT_URI, null, null);
				getContentResolver().delete(UserContent.CONTENT_URI, null, null);
				
				if(mRunning)
					connect();
			}
		}
	}
	
	private class UpdateLocationReceiver extends BroadcastReceiver
	{
		/* (non-Javadoc)
		 * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
		 */
		@Override
		public void onReceive(Context context, Intent intent)
		{
			Log.i(TAG, "Force location update request received.");
			LocationManager lm =  ((LocationManager)getSystemService(Context.LOCATION_SERVICE));
			
			Criteria crit = new Criteria();
			crit.setAccuracy(crit.getAccuracy());
			
			String prov = lm.getBestProvider(crit, true);
			Location lastPos = lm.getLastKnownLocation(prov);
			
			if(lastPos == null)
			{
				Log.i(TAG, "Failed to obtain recent location from: " + prov);
				return;
			}
			
			sendLocationUpdate(lastPos);
		}
	}
	
	private class AddFriendReceiver extends BroadcastReceiver
	{

		/* (non-Javadoc)
		 * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent)
		 */
		@Override
		public void onReceive(Context con, Intent intent)
		{
			String newName = intent.getStringExtra("username");
			User newFriend = new User(newName);
			
			synchronized (mRunLock)
			{
				if(mRunning && mSock != null && mSock.isConnected())
				{
					Log.i(TAG, "Updating server with new friend.");
					ClientRequest cr = new ClientRequest(RequestType.ADD_FRIEND, User.USER_ID_LEN);
					ServerResponse sr = new ServerResponse();
					
					try
					{
						send(cr.getServerRepresentation());
						sr.fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
						if(sr.getType() != ResponseType.REQ_OK)
							return;
						
						send(newFriend.getServerRepresentation());
						(sr = new ServerResponse()).fillWithTransmittedData(receive(ServerResponse.RESPONSE_LEN));
						if(sr.getType() != ResponseType.REQ_OK)
							return;
					}
					catch(Exception ex)
					{
						Log.i(TAG, "Failed to add new friend.");
						ex.printStackTrace();
						return;
					}
				}
			}
			
			Log.i(TAG, "New friend accepted by server.");
		}
	}
}
